package net.openadventure.animation;

import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;

import net.openadventure.State;
import net.openadventure.pathfinding.GridNode;
import net.openadventure.pathfinding.Pathfinder;
import net.openadventure.sprites.Ego;
import net.openadventure.sprites.Sprite;
import util.OAUtil;
import util.Point;

public class WalkEgo extends OneTimeMove {

	// adjust this to affect how far ahead the ego will look during path
	// finding.
	private final int MAX_NODE_COUNT = 5;

	private List<GridNode> path;
	private int pathIndex;

	private double distanceTraveledDuringCurrentUpdate = 0;

	public WalkEgo(int speed, int toX, int toY, State state) {
		super("ego", speed, 0, 0, toX, toY);

		System.out.println("WalkEgo to (" + toX + "," + toY + ")");

		generatePath(state);
	}

	private void generatePath(State state) {
		Ego ego = (Ego) state.getSprites().get("ego");

		// Generate the walkable grid
		Point[][] grid = getWalkableGrid(state, 15);

		Point egoCenter = ego.getActualCenter();
		path = Pathfinder.findPathOnGrid(state, egoCenter.getX(), egoCenter
				.getY(), toX, toY, grid, MAX_NODE_COUNT);
		pathIndex = 0;

		if (path.size() == 0) {
			super.toX = egoCenter.getX();
			super.toY = egoCenter.getY();
		} else {
			// only reset the toX and toY if we get back fewer nodes than we
			// asked for
			if (path.size() < MAX_NODE_COUNT) {
				GridNode last = path.get(path.size() - 1);
				super.toX = last.getX();
				super.toY = last.getY();
			}
		}

		// drawWaypoints(state);
		// drawGrid(state, grid);
	}

	@Override
	public void updateSprite(State state) {
		distanceTraveledDuringCurrentUpdate = 0;

		// if we have run out of current nodes with more to go, recalculate the
		// path
		if (pathIndex + 1 >= path.size()) {
			generatePath(state);

			// make sure we shortcut this method if the end is already reached.
			if (path.size() == 1) {
				return;
			}
		}

		// Make sure the placing the ego on the new waypoint won't make it's
		// footprint intersect the footprint of another sprite
		boolean spriteCollision = false;
		Ego ego = (Ego) state.getSprites().get("ego");
		Polygon footPrint = ego.getOriginalFootPrint();
		Area footPrintArea = new Area(footPrint);
		Point center = ego.getOriginalCenter();
		Point pathPoint = path.get(pathIndex + 1).getPoint();
		int deltaX = pathPoint.getX() - center.getX();
		int deltaY = pathPoint.getY() - center.getY();
		footPrint.translate(deltaX, deltaY);
		LinkedHashMap<String, Sprite> sprites = state.getSprites();
		for (Iterator<Sprite> iter = sprites.values().iterator(); iter
				.hasNext();) {
			Sprite otherSprite = iter.next();
			// Make sure the sprite isn't the ego, has a footprint, and
			// the footprint doesn't intersect the ego's footprint.
			if (!otherSprite.equals(ego)
					&& otherSprite.getActualFootPrint() != null
					&& pathIndex < path.size() - 1) {
				Area otherFootPrint = new Area(otherSprite.getActualFootPrint());
				otherFootPrint.intersect(footPrintArea);
				if (!otherFootPrint.isEmpty()) {
					spriteCollision = true;
				}
			}
		}

		// If the next waypoint falls within the footprint of another sprite,
		// recalculate the path
		if (spriteCollision) {
			generatePath(state);
		}

		// Calculate the next sprite
		Ego potentialEgo = getFinalUpdatedEgo(state);

		state.getSprites().put("ego", potentialEgo);
	}

	private Ego getFinalUpdatedEgo(State state) {
		distanceTraveledDuringCurrentUpdate = 0;

		Ego finalEgo = (Ego) state.getSprites().get("ego");

		// Make sure we use up all the "speed" available during this update. But
		// don't run out of path nodes.
		while (distanceTraveledDuringCurrentUpdate < speed
				&& pathIndex < path.size() - 1) {

			// This either returns an Ego located at node: pathIndex + 1 or as
			// close as it could get during this update with the remaining
			// "speed"
			finalEgo = getCandidateUpdatedEgo(finalEgo, path.get(pathIndex)
					.getPoint(), path.get(pathIndex + 1).getPoint());

			// If the distance traveled is less than the total distance, we were
			// stopped at the last endPoint and we have to move towards the next
			// endpoint.
			if (distanceTraveledDuringCurrentUpdate < speed) {
				pathIndex++;
			}
		}

		return finalEgo;
	}

	/**
	 * Moves the ego in a straight line from the startPoint to the endPoint
	 * until the distance moved during the entire update is equal to the speed
	 * or the endPoint is reached.
	 * 
	 * @param ego
	 * @param startPoint
	 * @param endPoint
	 * @return
	 */
	private Ego getCandidateUpdatedEgo(Ego ego, Point startPoint, Point endPoint) {
		OAUtil.verifyNotNull(ego, "ego");
		OAUtil.verifyNotNull(startPoint, "startPoint");
		OAUtil.verifyNotNull(endPoint, "endPoint");

		Point egoCenter = ego.getActualCenter();

		double distanceToEndPoint = egoCenter.distance(endPoint);
		double remainingDistance = speed - distanceTraveledDuringCurrentUpdate;

		// If moving the total distance won't exhaust our speed budget, shortcut
		// the method and just move directly to the endPoint.
		if (distanceToEndPoint <= remainingDistance) {
			distanceTraveledDuringCurrentUpdate += distanceToEndPoint;

			// Place the ego footprint center directly on top of the endpoint.
			return new Ego(ego.getImagePath(), endPoint.getX()
					- ego.getOriginalCenter().getX(), endPoint.getY()
					- ego.getOriginalCenter().getY(), ego.getOriginalCenter(),
					ego.getOriginalFootPrint(), ego.getWidth(), ego.getHeight());
		}

		int xMove = 0;
		int yMove = 0;
		int xDistance = (endPoint.getX() - egoCenter.getX());
		int yDistance = (endPoint.getY() - egoCenter.getY());
		int totalXDistance = (endPoint.getX() - startPoint.getX());
		int totalYDistance = (endPoint.getY() - startPoint.getY());

		if (Math.abs(yDistance) < Math.abs(xDistance)) {
			// X math here
			double unitXDistance = xDistance / distanceToEndPoint;
			xMove = (int) (remainingDistance * unitXDistance);
			// Adjust to maintain equal ratios
			double xMovedRatio = (egoCenter.getX() + xMove - startPoint.getX())
					/ (double) totalXDistance;
			// adjust yMove to make them equal
			yMove = (int) (xMovedRatio * totalYDistance + startPoint.getY() - egoCenter
					.getY());
		} else {
			// Y math here
			double unitYDistance = yDistance / distanceToEndPoint;
			yMove = (int) (remainingDistance * unitYDistance);
			double yMovedRatio = (egoCenter.getY() + yMove - startPoint.getY())
					/ (double) totalYDistance;
			// adjust xMove to make them equal
			xMove = (int) (yMovedRatio * totalXDistance + startPoint.getX() - egoCenter
					.getX());
		}

		distanceTraveledDuringCurrentUpdate += remainingDistance;
		return new Ego(ego.getImagePath(), ego.getX() + xMove, ego.getY()
				+ yMove, ego.getOriginalCenter(), ego.getOriginalFootPrint(),
				ego.getWidth(), ego.getHeight());
	}

	@SuppressWarnings("unused")
	private void drawWaypoints(State state) {
		// Remove any old path markers
		int index = 0;
		while (state.getSprites().get("path" + index) != null) {
			Sprite old = state.getSprites().remove("path" + index);

			if (old == null) {
				System.out.println("Failed to remove one");
			}

			index++;
		}

		// Add new path markers
		index = 0;
		for (Iterator<GridNode> it = path.iterator(); it.hasNext();) {
			GridNode temp = it.next();
			state.getSprites().put(
					"path" + index,
					new Sprite("images/test.gif", temp.getX() - 2,
							temp.getY() - 2, null, null, 5, 5, false, false));
			index++;
		}
	}

	// Unrelated helper functions that should probably go in a separate class
	private Point[][] getWalkableGrid(State state, int spacing) {
		System.out.println("Generating Walkable Grid...");

		Polygon walkableArea = state.getCurrentLocation()
				.getCurrentWalkableArea().getPolygon();
		Rectangle bounds = walkableArea.getBounds();

		int gridWidth = (int) (bounds.getWidth() / spacing);
		int gridHeight = (int) (bounds.getHeight() / spacing);
		Point[][] grid = new Point[gridWidth][gridHeight];

		for (int row = 0; row < gridHeight; row++) {
			for (int col = 0; col < gridWidth; col++) {
				int x = (int) (bounds.getX() + col * spacing);
				int y = (int) (bounds.getY() + row * spacing);
				Point point = new Point(x, y);

				// Make sure placing the footprint on the point doesn't make it
				// exit the walkable area
				// Get the footprint and "center" it over the point
				Ego ego = (Ego) state.getSprites().get("ego");
				Polygon footPrint = ego.getOriginalFootPrint();
				Point center = ego.getOriginalCenter();
				int deltaX = point.getX() - center.getX();
				int deltaY = point.getY() - center.getY();
				footPrint.translate(deltaX, deltaY);
				Area footPrintArea = new Area(footPrint);
				// Check that all points in the footprint are contained by the
				// walkable area
				boolean footprintInArea = true;
				// TODO: do an actual AREA intersection check here
				for (int i = 0; i < footPrint.npoints; i++) {
					if (!walkableArea.contains(new Point(footPrint.xpoints[i],
							footPrint.ypoints[i]).getPoint2D())) {
						footprintInArea = false;
					}
				}

				// Make sure that placing the ego on the point won't make it's
				// footprint intersect any footprints of other sprites
				boolean noSpriteCollisions = true;
				LinkedHashMap<String, Sprite> sprites = state.getSprites();
				for (Iterator<Sprite> iter = sprites.values().iterator(); iter
						.hasNext();) {

					Sprite otherSprite = iter.next();
					// Make sure the sprite isn't the ego, has a footprint, and
					// the footprint doesn't intersect the ego's footprint.
					if (!otherSprite.equals(ego)
							&& otherSprite.getActualFootPrint() != null) {
						Area otherFootPrint = new Area(otherSprite
								.getActualFootPrint());
						otherFootPrint.intersect(footPrintArea);
						if (!otherFootPrint.isEmpty()) {
							noSpriteCollisions = false;
						}
					}
				}

				if (footprintInArea && noSpriteCollisions) {
					grid[col][row] = point;
				}
			}
		}

		return grid;
	}

	@SuppressWarnings("unused")
	private void drawGrid(State state, Point[][] grid) {
		// Remove any old path markers
		int index = 0;
		while (state.getSprites().get("grid" + index) != null) {
			Sprite old = state.getSprites().remove("grid" + index);

			if (old == null) {
				System.out.println("Failed to remove one");
			}

			index++;
		}

		// Add new path markers
		index = 0;

		for (int row = 0; row < grid[0].length; row++) {
			for (int col = 0; col < grid.length; col++) {
				if (grid[col][row] != null) {
					state.getSprites().put(
							"grid" + index,
							new Sprite("images/test.gif",
									grid[col][row].getX() - 2, grid[col][row]
											.getY() - 2, null, null, 5, 5,
									true, true));
					index++;
				}
			}
		}
	}
}
